/*
 * $HeadURL: https://svn.apache.org/repos/asf/jakarta/httpcomponents/oac.hc3x/tags/HTTPCLIENT_3_1/src/java/org/apache/commons/httpclient/cookie/RFC2965Spec.java $
 * $Revision: 507134 $
 * $Date: 2007-02-13 19:18:05 +0100 (Tue, 13 Feb 2007) $
 * 
 * ====================================================================
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.commons.httpclient.cookie;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HeaderElement;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.util.ParameterFormatter;

/**
 * <p>
 * RFC 2965 specific cookie management functions.
 * </p>
 * 
 * @author jain.samit@gmail.com (Samit Jain)
 * 
 * @since 3.1
 */
public class RFC2965Spec extends CookieSpecBase implements CookieVersionSupport {

	private static final Comparator PATH_COMPOARATOR = new CookiePathComparator();

	/**
	 * Cookie Response Header name for cookies processed by this spec.
	 */
	public final static String SET_COOKIE2_KEY = "set-cookie2";

	/**
	 * used for formatting RFC 2956 style cookies
	 */
	private final ParameterFormatter formatter;

	/**
	 * Stores the list of attribute handlers
	 */
	private final List attribHandlerList;

	/**
	 * Stores attribute name -> attribute handler mappings
	 */
	private final Map attribHandlerMap;

	/**
	 * Fallback cookie spec (RFC 2109)
	 */
	private final CookieSpec rfc2109;

	/**
	 * Default constructor
	 * */
	public RFC2965Spec() {
		super();
		this.formatter = new ParameterFormatter();
		this.formatter.setAlwaysUseQuotes(true);
		this.attribHandlerMap = new HashMap(10);
		this.attribHandlerList = new ArrayList(10);
		this.rfc2109 = new RFC2109Spec();

		registerAttribHandler(Cookie2.PATH, new Cookie2PathAttributeHandler());
		registerAttribHandler(Cookie2.DOMAIN,
				new Cookie2DomainAttributeHandler());
		registerAttribHandler(Cookie2.PORT, new Cookie2PortAttributeHandler());
		registerAttribHandler(Cookie2.MAXAGE,
				new Cookie2MaxageAttributeHandler());
		registerAttribHandler(Cookie2.SECURE,
				new CookieSecureAttributeHandler());
		registerAttribHandler(Cookie2.COMMENT,
				new CookieCommentAttributeHandler());
		registerAttribHandler(Cookie2.COMMENTURL,
				new CookieCommentUrlAttributeHandler());
		registerAttribHandler(Cookie2.DISCARD,
				new CookieDiscardAttributeHandler());
		registerAttribHandler(Cookie2.VERSION,
				new Cookie2VersionAttributeHandler());
	}

	protected void registerAttribHandler(final String name,
			final CookieAttributeHandler handler) {
		if (name == null) {
			throw new IllegalArgumentException("Attribute name may not be null");
		}
		if (handler == null) {
			throw new IllegalArgumentException(
					"Attribute handler may not be null");
		}
		if (!this.attribHandlerList.contains(handler)) {
			this.attribHandlerList.add(handler);
		}
		this.attribHandlerMap.put(name, handler);
	}

	/**
	 * Finds an attribute handler {@link CookieAttributeHandler} for the given
	 * attribute. Returns <tt>null</tt> if no attribute handler is found for the
	 * specified attribute.
	 * 
	 * @param name
	 *            attribute name. e.g. Domain, Path, etc.
	 * @return an attribute handler or <tt>null</tt>
	 */
	protected CookieAttributeHandler findAttribHandler(final String name) {
		return (CookieAttributeHandler) this.attribHandlerMap.get(name);
	}

	/**
	 * Gets attribute handler {@link CookieAttributeHandler} for the given
	 * attribute.
	 * 
	 * @param name
	 *            attribute name. e.g. Domain, Path, etc.
	 * @throws IllegalStateException
	 *             if handler not found for the specified attribute.
	 */
	protected CookieAttributeHandler getAttribHandler(final String name) {
		CookieAttributeHandler handler = findAttribHandler(name);
		if (handler == null) {
			throw new IllegalStateException("Handler not registered for "
					+ name + " attribute.");
		} else {
			return handler;
		}
	}

	protected Iterator getAttribHandlerIterator() {
		return this.attribHandlerList.iterator();
	}

/**
     * Parses the Set-Cookie2 value into an array of <tt>Cookie</tt>s.
     *
     * <P>The syntax for the Set-Cookie2 response header is:
     *
     * <PRE>
     * set-cookie      =    "Set-Cookie2:" cookies
     * cookies         =    1#cookie
     * cookie          =    NAME "=" VALUE * (";" cookie-av)
     * NAME            =    attr
     * VALUE           =    value
     * cookie-av       =    "Comment" "=" value
     *                 |    "CommentURL" "=" <"> http_URL <">
     *                 |    "Discard"
     *                 |    "Domain" "=" value
     *                 |    "Max-Age" "=" value
     *                 |    "Path" "=" value
     *                 |    "Port" [ "=" <"> portlist <"> ]
     *                 |    "Secure"
     *                 |    "Version" "=" 1*DIGIT
     * portlist        =       1#portnum
     * portnum         =       1*DIGIT
     * </PRE>
     *
     * @param host the host from which the <tt>Set-Cookie2</tt> value was
     * received
     * @param port the port from which the <tt>Set-Cookie2</tt> value was
     * received
     * @param path the path from which the <tt>Set-Cookie2</tt> value was
     * received
     * @param secure <tt>true</tt> when the <tt>Set-Cookie2</tt> value was
     * received over secure conection
     * @param header the <tt>Set-Cookie2</tt> <tt>Header</tt> received from the server
     * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie2 value
     * @throws MalformedCookieException if an exception occurs during parsing
     */
	public Cookie[] parse(String host, int port, String path, boolean secure,
			final Header header) throws MalformedCookieException {
		// LOG.trace("enter RFC2965.parse("
		// + "String, int, String, boolean, Header)");

		if (header == null) {
			throw new IllegalArgumentException("Header may not be null.");
		}
		if (header.getName() == null) {
			throw new IllegalArgumentException("Header name may not be null.");
		}

		if (header.getName().equalsIgnoreCase(SET_COOKIE2_KEY)) {
			// parse cookie2 cookies
			return parse(host, port, path, secure, header.getValue());
		} else if (header.getName()
				.equalsIgnoreCase(RFC2109Spec.SET_COOKIE_KEY)) {
			// delegate parsing of old-style cookies to rfc2109Spec
			return this.rfc2109.parse(host, port, path, secure,
					header.getValue());
		} else {
			throw new MalformedCookieException("Header name is not valid. "
					+ "RFC 2965 supports \"set-cookie\" "
					+ "and \"set-cookie2\" headers.");
		}
	}

	/**
	 * @see #parse(String, int, String, boolean,
	 *      org.apache.commons.httpclient.Header)
	 */
	public Cookie[] parse(String host, int port, String path, boolean secure,
			final String header) throws MalformedCookieException {
		// LOG.trace("enter RFC2965Spec.parse("
		// + "String, int, String, boolean, String)");

		// before we do anything, lets check validity of arguments
		if (host == null) {
			throw new IllegalArgumentException("Host of origin may not be null");
		}
		if (host.trim().equals("")) {
			throw new IllegalArgumentException(
					"Host of origin may not be blank");
		}
		if (port < 0) {
			throw new IllegalArgumentException("Invalid port: " + port);
		}
		if (path == null) {
			throw new IllegalArgumentException(
					"Path of origin may not be null.");
		}
		if (header == null) {
			throw new IllegalArgumentException("Header may not be null.");
		}

		if (path.trim().equals("")) {
			path = PATH_DELIM;
		}
		host = getEffectiveHost(host);

		HeaderElement[] headerElements = HeaderElement.parseElements(header
				.toCharArray());

		List cookies = new LinkedList();
		for (int i = 0; i < headerElements.length; i++) {
			HeaderElement headerelement = headerElements[i];
			Cookie2 cookie = null;
			try {
				cookie = new Cookie2(host, headerelement.getName(),
						headerelement.getValue(), path, null, false,
						new int[] { port });
			} catch (IllegalArgumentException ex) {
				throw new MalformedCookieException(ex.getMessage());
			}
			NameValuePair[] parameters = headerelement.getParameters();
			// could be null. In case only a header element and no parameters.
			if (parameters != null) {
				// Eliminate duplicate attribues. The first occurence takes
				// precedence
				Map attribmap = new HashMap(parameters.length);
				for (int j = parameters.length - 1; j >= 0; j--) {
					NameValuePair param = parameters[j];
					attribmap.put(param.getName().toLowerCase(), param);
				}
				for (Iterator it = attribmap.entrySet().iterator(); it
						.hasNext();) {
					Map.Entry entry = (Map.Entry) it.next();
					parseAttribute((NameValuePair) entry.getValue(), cookie);
				}
			}
			cookies.add(cookie);
			// cycle through the parameters
		}
		return (Cookie[]) cookies.toArray(new Cookie[cookies.size()]);
	}

	/**
	 * Parse RFC 2965 specific cookie attribute and update the corresponsing
	 * {@link org.apache.commons.httpclient.Cookie} properties.
	 * 
	 * @param attribute
	 *            {@link org.apache.commons.httpclient.NameValuePair} cookie
	 *            attribute from the <tt>Set-Cookie2</tt> header.
	 * @param cookie
	 *            {@link org.apache.commons.httpclient.Cookie} to be updated
	 * @throws MalformedCookieException
	 *             if an exception occurs during parsing
	 */
	public void parseAttribute(final NameValuePair attribute,
			final Cookie cookie) throws MalformedCookieException {
		if (attribute == null) {
			throw new IllegalArgumentException("Attribute may not be null.");
		}
		if (attribute.getName() == null) {
			throw new IllegalArgumentException(
					"Attribute Name may not be null.");
		}
		if (cookie == null) {
			throw new IllegalArgumentException("Cookie may not be null.");
		}
		final String paramName = attribute.getName().toLowerCase();
		final String paramValue = attribute.getValue();

		CookieAttributeHandler handler = findAttribHandler(paramName);
		if (handler == null) {
			// ignore unknown attribute-value pairs
			// if (LOG.isDebugEnabled())
			// LOG.debug("Unrecognized cookie attribute: " +
			// attribute.toString());
		} else {
			handler.parse(cookie, paramValue);
		}
	}

	/**
	 * Performs RFC 2965 compliant {@link org.apache.commons.httpclient.Cookie}
	 * validation
	 * 
	 * @param host
	 *            the host from which the
	 *            {@link org.apache.commons.httpclient.Cookie} was received
	 * @param port
	 *            the port from which the
	 *            {@link org.apache.commons.httpclient.Cookie} was received
	 * @param path
	 *            the path from which the
	 *            {@link org.apache.commons.httpclient.Cookie} was received
	 * @param secure
	 *            <tt>true</tt> when the
	 *            {@link org.apache.commons.httpclient.Cookie} was received
	 *            using a secure connection
	 * @param cookie
	 *            The cookie to validate
	 * @throws MalformedCookieException
	 *             if an exception occurs during validation
	 */
	public void validate(final String host, int port, final String path,
			boolean secure, final Cookie cookie)
			throws MalformedCookieException {

		// LOG.trace("enter RFC2965Spec.validate(String, int, String, "
		// + "boolean, Cookie)");

		if (cookie instanceof Cookie2) {
			if (cookie.getName().indexOf(' ') != -1) {
				throw new MalformedCookieException(
						"Cookie name may not contain blanks");
			}
			if (cookie.getName().startsWith("$")) {
				throw new MalformedCookieException(
						"Cookie name may not start with $");
			}
			CookieOrigin origin = new CookieOrigin(getEffectiveHost(host),
					port, path, secure);
			for (Iterator i = getAttribHandlerIterator(); i.hasNext();) {
				CookieAttributeHandler handler = (CookieAttributeHandler) i
						.next();
				handler.validate(cookie, origin);
			}
		} else {
			// old-style cookies are validated according to the old rules
			this.rfc2109.validate(host, port, path, secure, cookie);
		}
	}

	/**
	 * Return <tt>true</tt> if the cookie should be submitted with a request
	 * with given attributes, <tt>false</tt> otherwise.
	 * 
	 * @param host
	 *            the host to which the request is being submitted
	 * @param port
	 *            the port to which the request is being submitted (ignored)
	 * @param path
	 *            the path to which the request is being submitted
	 * @param secure
	 *            <tt>true</tt> if the request is using a secure connection
	 * @return true if the cookie matches the criterium
	 */
	public boolean match(String host, int port, String path, boolean secure,
			final Cookie cookie) {

		// LOG.trace("enter RFC2965.match("
		// + "String, int, String, boolean, Cookie");
		if (cookie == null) {
			throw new IllegalArgumentException("Cookie may not be null");
		}
		if (cookie instanceof Cookie2) {
			// check if cookie has expired
			if (cookie.isPersistent() && cookie.isExpired()) {
				return false;
			}
			CookieOrigin origin = new CookieOrigin(getEffectiveHost(host),
					port, path, secure);
			for (Iterator i = getAttribHandlerIterator(); i.hasNext();) {
				CookieAttributeHandler handler = (CookieAttributeHandler) i
						.next();
				if (!handler.match(cookie, origin)) {
					return false;
				}
			}
			return true;
		} else {
			// old-style cookies are matched according to the old rules
			return this.rfc2109.match(host, port, path, secure, cookie);
		}
	}

	private void doFormatCookie2(final Cookie2 cookie, final StringBuffer buffer) {
		String name = cookie.getName();
		String value = cookie.getValue();
		if (value == null) {
			value = "";
		}
		this.formatter.format(buffer, new NameValuePair(name, value));
		// format domain attribute
		if (cookie.getDomain() != null && cookie.isDomainAttributeSpecified()) {
			buffer.append("; ");
			this.formatter.format(buffer,
					new NameValuePair("$Domain", cookie.getDomain()));
		}
		// format path attribute
		if ((cookie.getPath() != null) && (cookie.isPathAttributeSpecified())) {
			buffer.append("; ");
			this.formatter.format(buffer,
					new NameValuePair("$Path", cookie.getPath()));
		}
		// format port attribute
		if (cookie.isPortAttributeSpecified()) {
			String portValue = "";
			if (!cookie.isPortAttributeBlank()) {
				portValue = createPortAttribute(cookie.getPorts());
			}
			buffer.append("; ");
			this.formatter
					.format(buffer, new NameValuePair("$Port", portValue));
		}
	}

	/**
	 * Return a string suitable for sending in a <tt>"Cookie"</tt> header as
	 * defined in RFC 2965
	 * 
	 * @param cookie
	 *            a {@link org.apache.commons.httpclient.Cookie} to be formatted
	 *            as string
	 * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
	 */
	public String formatCookie(final Cookie cookie) {
		// LOG.trace("enter RFC2965Spec.formatCookie(Cookie)");

		if (cookie == null) {
			throw new IllegalArgumentException("Cookie may not be null");
		}
		if (cookie instanceof Cookie2) {
			Cookie2 cookie2 = (Cookie2) cookie;
			int version = cookie2.getVersion();
			final StringBuffer buffer = new StringBuffer();
			this.formatter.format(buffer,
					new NameValuePair("$Version", Integer.toString(version)));
			buffer.append("; ");
			doFormatCookie2(cookie2, buffer);
			return buffer.toString();
		} else {
			// old-style cookies are formatted according to the old rules
			return this.rfc2109.formatCookie(cookie);
		}
	}

	/**
	 * Create a RFC 2965 compliant <tt>"Cookie"</tt> header value containing all
	 * {@link org.apache.commons.httpclient.Cookie}s suitable for sending in a
	 * <tt>"Cookie"</tt> header
	 * 
	 * @param cookies
	 *            an array of {@link org.apache.commons.httpclient.Cookie}s to
	 *            be formatted
	 * @return a string suitable for sending in a Cookie header.
	 */
	public String formatCookies(final Cookie[] cookies) {
		// LOG.trace("enter RFC2965Spec.formatCookieHeader(Cookie[])");

		if (cookies == null) {
			throw new IllegalArgumentException("Cookies may not be null");
		}
		// check if cookies array contains a set-cookie (old style) cookie
		boolean hasOldStyleCookie = false;
		int version = -1;
		for (int i = 0; i < cookies.length; i++) {
			Cookie cookie = cookies[i];
			if (!(cookie instanceof Cookie2)) {
				hasOldStyleCookie = true;
				break;
			}
			if (cookie.getVersion() > version) {
				version = cookie.getVersion();
			}
		}
		if (version < 0) {
			version = 0;
		}
		if (hasOldStyleCookie || version < 1) {
			// delegate old-style cookie formatting to rfc2109Spec
			return this.rfc2109.formatCookies(cookies);
		}
		// Arrange cookies by path
		Arrays.sort(cookies, PATH_COMPOARATOR);

		final StringBuffer buffer = new StringBuffer();
		// format cookie version
		this.formatter.format(buffer,
				new NameValuePair("$Version", Integer.toString(version)));
		for (int i = 0; i < cookies.length; i++) {
			buffer.append("; ");
			Cookie2 cookie = (Cookie2) cookies[i];
			// format cookie attributes
			doFormatCookie2(cookie, buffer);
		}
		return buffer.toString();
	}

	/**
	 * Retrieves valid Port attribute value for the given ports array. e.g.
	 * "8000,8001,8002"
	 * 
	 * @param ports
	 *            int array of ports
	 */
	private String createPortAttribute(int[] ports) {
		StringBuffer portValue = new StringBuffer();
		for (int i = 0, len = ports.length; i < len; i++) {
			if (i > 0) {
				portValue.append(",");
			}
			portValue.append(ports[i]);
		}
		return portValue.toString();
	}

	/**
	 * Parses the given Port attribute value (e.g. "8000,8001,8002") into an
	 * array of ports.
	 * 
	 * @param portValue
	 *            port attribute value
	 * @return parsed array of ports
	 * @throws MalformedCookieException
	 *             if there is a problem in parsing due to invalid portValue.
	 */
	private int[] parsePortAttribute(final String portValue)
			throws MalformedCookieException {
		StringTokenizer st = new StringTokenizer(portValue, ",");
		int[] ports = new int[st.countTokens()];
		try {
			int i = 0;
			while (st.hasMoreTokens()) {
				ports[i] = Integer.parseInt(st.nextToken().trim());
				if (ports[i] < 0) {
					throw new MalformedCookieException(
							"Invalid Port attribute.");
				}
				++i;
			}
		} catch (NumberFormatException e) {
			throw new MalformedCookieException("Invalid Port " + "attribute: "
					+ e.getMessage());
		}
		return ports;
	}

	/**
	 * Gets 'effective host name' as defined in RFC 2965.
	 * <p>
	 * If a host name contains no dots, the effective host name is that name
	 * with the string .local appended to it. Otherwise the effective host name
	 * is the same as the host name. Note that all effective host names contain
	 * at least one dot.
	 * 
	 * @param host
	 *            host name where cookie is received from or being sent to.
	 * @return
	 */
	private static String getEffectiveHost(final String host) {
		String effectiveHost = host.toLowerCase();
		if (host.indexOf('.') < 0) {
			effectiveHost += ".local";
		}
		return effectiveHost;
	}

	/**
	 * Performs domain-match as defined by the RFC2965.
	 * <p>
	 * Host A's name domain-matches host B's if
	 * <ol>
	 * <ul>
	 * their host name strings string-compare equal; or
	 * </ul>
	 * <ul>
	 * A is a HDN string and has the form NB, where N is a non-empty name
	 * string, B has the form .B', and B' is a HDN string. (So, x.y.com
	 * domain-matches .Y.com but not Y.com.)
	 * </ul>
	 * </ol>
	 * 
	 * @param host
	 *            host name where cookie is received from or being sent to.
	 * @param domain
	 *            The cookie domain attribute.
	 * @return true if the specified host matches the given domain.
	 */
	public boolean domainMatch(String host, String domain) {
		boolean match = host.equals(domain)
				|| (domain.startsWith(".") && host.endsWith(domain));

		return match;
	}

	/**
	 * Returns <tt>true</tt> if the given port exists in the given ports list.
	 * 
	 * @param port
	 *            port of host where cookie was received from or being sent to.
	 * @param ports
	 *            port list
	 * @return true returns <tt>true</tt> if the given port exists in the given
	 *         ports list; <tt>false</tt> otherwise.
	 */
	private boolean portMatch(int port, int[] ports) {
		boolean portInList = false;
		for (int i = 0, len = ports.length; i < len; i++) {
			if (port == ports[i]) {
				portInList = true;
				break;
			}
		}
		return portInList;
	}

	/**
	 * <tt>"Path"</tt> attribute handler for RFC 2965 cookie spec.
	 */
	private class Cookie2PathAttributeHandler implements CookieAttributeHandler {

		/**
		 * Parse cookie path attribute.
		 */
		public void parse(final Cookie cookie, final String path)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (path == null) {
				throw new MalformedCookieException(
						"Missing value for path attribute");
			}
			if (path.trim().equals("")) {
				throw new MalformedCookieException(
						"Blank value for path attribute");
			}
			cookie.setPath(path);
			cookie.setPathAttributeSpecified(true);
		}

		/**
		 * Validate cookie path attribute. The value for the Path attribute must
		 * be a prefix of the request-URI (case-sensitive matching).
		 */
		public void validate(final Cookie cookie, final CookieOrigin origin)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (origin == null) {
				throw new IllegalArgumentException(
						"Cookie origin may not be null");
			}
			String path = origin.getPath();
			if (path == null) {
				throw new IllegalArgumentException(
						"Path of origin host may not be null.");
			}
			if (cookie.getPath() == null) {
				throw new MalformedCookieException("Invalid cookie state: "
						+ "path attribute is null.");
			}
			if (path.trim().equals("")) {
				path = PATH_DELIM;
			}

			if (!pathMatch(path, cookie.getPath())) {
				throw new MalformedCookieException("Illegal path attribute \""
						+ cookie.getPath() + "\". Path of origin: \"" + path
						+ "\"");
			}
		}

		/**
		 * Match cookie path attribute. The value for the Path attribute must be
		 * a prefix of the request-URI (case-sensitive matching).
		 */
		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (origin == null) {
				throw new IllegalArgumentException(
						"Cookie origin may not be null");
			}
			String path = origin.getPath();
			if (cookie.getPath() == null) {
				// LOG.warn("Invalid cookie state: path attribute is null.");
				return false;
			}
			if (path.trim().equals("")) {
				path = PATH_DELIM;
			}

			if (!pathMatch(path, cookie.getPath())) {
				return false;
			}
			return true;
		}
	}

	/**
	 * <tt>"Domain"</tt> cookie attribute handler for RFC 2965 cookie spec.
	 */
	private class Cookie2DomainAttributeHandler implements
			CookieAttributeHandler {

		/**
		 * Parse cookie domain attribute.
		 */
		public void parse(final Cookie cookie, String domain)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (domain == null) {
				throw new MalformedCookieException(
						"Missing value for domain attribute");
			}
			if (domain.trim().equals("")) {
				throw new MalformedCookieException(
						"Blank value for domain attribute");
			}
			domain = domain.toLowerCase();
			if (!domain.startsWith(".")) {
				// Per RFC 2965 section 3.2.2
				// "... If an explicitly specified value does not start with
				// a dot, the user agent supplies a leading dot ..."
				// That effectively implies that the domain attribute
				// MAY NOT be an IP address of a host name
				domain = "." + domain;
			}
			cookie.setDomain(domain);
			cookie.setDomainAttributeSpecified(true);
		}

		/**
		 * Validate cookie domain attribute.
		 */
		public void validate(final Cookie cookie, final CookieOrigin origin)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (origin == null) {
				throw new IllegalArgumentException(
						"Cookie origin may not be null");
			}
			String host = origin.getHost().toLowerCase();
			if (cookie.getDomain() == null) {
				throw new MalformedCookieException("Invalid cookie state: "
						+ "domain not specified");
			}
			String cookieDomain = cookie.getDomain().toLowerCase();

			if (cookie.isDomainAttributeSpecified()) {
				// Domain attribute must start with a dot
				if (!cookieDomain.startsWith(".")) {
					throw new MalformedCookieException(
							"Domain attribute \""
									+ cookie.getDomain()
									+ "\" violates RFC 2109: domain must start with a dot");
				}

				// Domain attribute must contain atleast one embedded dot,
				// or the value must be equal to .local.
				int dotIndex = cookieDomain.indexOf('.', 1);
				if (((dotIndex < 0) || (dotIndex == cookieDomain.length() - 1))
						&& (!cookieDomain.equals(".local"))) {
					throw new MalformedCookieException(
							"Domain attribute \""
									+ cookie.getDomain()
									+ "\" violates RFC 2965: the value contains no embedded dots "
									+ "and the value is not .local");
				}

				// The effective host name must domain-match domain attribute.
				if (!domainMatch(host, cookieDomain)) {
					throw new MalformedCookieException(
							"Domain attribute \""
									+ cookie.getDomain()
									+ "\" violates RFC 2965: effective host name does not "
									+ "domain-match domain attribute.");
				}

				// effective host name minus domain must not contain any dots
				String effectiveHostWithoutDomain = host.substring(0,
						host.length() - cookieDomain.length());
				if (effectiveHostWithoutDomain.indexOf('.') != -1) {
					throw new MalformedCookieException(
							"Domain attribute \""
									+ cookie.getDomain()
									+ "\" violates RFC 2965: "
									+ "effective host minus domain may not contain any dots");
				}
			} else {
				// Domain was not specified in header. In this case, domain must
				// string match request host (case-insensitive).
				if (!cookie.getDomain().equals(host)) {
					throw new MalformedCookieException(
							"Illegal domain attribute: \"" + cookie.getDomain()
									+ "\"." + "Domain of origin: \"" + host
									+ "\"");
				}
			}
		}

		/**
		 * Match cookie domain attribute.
		 */
		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (origin == null) {
				throw new IllegalArgumentException(
						"Cookie origin may not be null");
			}
			String host = origin.getHost().toLowerCase();
			String cookieDomain = cookie.getDomain();

			// The effective host name MUST domain-match the Domain
			// attribute of the cookie.
			if (!domainMatch(host, cookieDomain)) {
				return false;
			}
			// effective host name minus domain must not contain any dots
			String effectiveHostWithoutDomain = host.substring(0, host.length()
					- cookieDomain.length());
			if (effectiveHostWithoutDomain.indexOf('.') != -1) {
				return false;
			}
			return true;
		}

	}

	/**
	 * <tt>"Port"</tt> cookie attribute handler for RFC 2965 cookie spec.
	 */
	private class Cookie2PortAttributeHandler implements CookieAttributeHandler {

		/**
		 * Parse cookie port attribute.
		 */
		public void parse(final Cookie cookie, final String portValue)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (cookie instanceof Cookie2) {
				Cookie2 cookie2 = (Cookie2) cookie;
				if ((portValue == null) || (portValue.trim().equals(""))) {
					// If the Port attribute is present but has no value, the
					// cookie can only be sent to the request-port.
					// Since the default port list contains only request-port,
					// we don't
					// need to do anything here.
					cookie2.setPortAttributeBlank(true);
				} else {
					int[] ports = parsePortAttribute(portValue);
					cookie2.setPorts(ports);
				}
				cookie2.setPortAttributeSpecified(true);
			}
		}

		/**
		 * Validate cookie port attribute. If the Port attribute was specified
		 * in header, the request port must be in cookie's port list.
		 */
		public void validate(final Cookie cookie, final CookieOrigin origin)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (origin == null) {
				throw new IllegalArgumentException(
						"Cookie origin may not be null");
			}
			if (cookie instanceof Cookie2) {
				Cookie2 cookie2 = (Cookie2) cookie;
				int port = origin.getPort();
				if (cookie2.isPortAttributeSpecified()) {
					if (!portMatch(port, cookie2.getPorts())) {
						throw new MalformedCookieException(
								"Port attribute violates RFC 2965: "
										+ "Request port not found in cookie's port list.");
					}
				}
			}
		}

		/**
		 * Match cookie port attribute. If the Port attribute is not specified
		 * in header, the cookie can be sent to any port. Otherwise, the request
		 * port must be in the cookie's port list.
		 */
		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (origin == null) {
				throw new IllegalArgumentException(
						"Cookie origin may not be null");
			}
			if (cookie instanceof Cookie2) {
				Cookie2 cookie2 = (Cookie2) cookie;
				int port = origin.getPort();
				if (cookie2.isPortAttributeSpecified()) {
					if (cookie2.getPorts() == null) {
						// LOG.warn("Invalid cookie state: port not specified");
						return false;
					}
					if (!portMatch(port, cookie2.getPorts())) {
						return false;
					}
				}
				return true;
			} else {
				return false;
			}
		}
	}

	/**
	 * <tt>"Max-age"</tt> cookie attribute handler for RFC 2965 cookie spec.
	 */
	private class Cookie2MaxageAttributeHandler implements
			CookieAttributeHandler {

		/**
		 * Parse cookie max-age attribute.
		 */
		public void parse(final Cookie cookie, final String value)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (value == null) {
				throw new MalformedCookieException(
						"Missing value for max-age attribute");
			}
			int age = -1;
			try {
				age = Integer.parseInt(value);
			} catch (NumberFormatException e) {
				age = -1;
			}
			if (age < 0) {
				throw new MalformedCookieException("Invalid max-age attribute.");
			}
			cookie.setExpiryDate(new Date(System.currentTimeMillis() + age
					* 1000L));
		}

		/**
		 * validate cookie max-age attribute.
		 */
		public void validate(final Cookie cookie, final CookieOrigin origin) {
		}

		/**
		 * @see CookieAttributeHandler#match(org.apache.commons.httpclient.Cookie,
		 *      String)
		 */
		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			return true;
		}

	}

	/**
	 * <tt>"Secure"</tt> cookie attribute handler for RFC 2965 cookie spec.
	 */
	private class CookieSecureAttributeHandler implements
			CookieAttributeHandler {

		public void parse(final Cookie cookie, final String secure)
				throws MalformedCookieException {
			cookie.setSecure(true);
		}

		public void validate(final Cookie cookie, final CookieOrigin origin)
				throws MalformedCookieException {
		}

		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (origin == null) {
				throw new IllegalArgumentException(
						"Cookie origin may not be null");
			}
			return cookie.getSecure() == origin.isSecure();
		}

	}

	/**
	 * <tt>"Commant"</tt> cookie attribute handler for RFC 2965 cookie spec.
	 */
	private class CookieCommentAttributeHandler implements
			CookieAttributeHandler {

		public void parse(final Cookie cookie, final String comment)
				throws MalformedCookieException {
			cookie.setComment(comment);
		}

		public void validate(final Cookie cookie, final CookieOrigin origin)
				throws MalformedCookieException {
		}

		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			return true;
		}

	}

	/**
	 * <tt>"CommantURL"</tt> cookie attribute handler for RFC 2965 cookie spec.
	 */
	private class CookieCommentUrlAttributeHandler implements
			CookieAttributeHandler {

		public void parse(final Cookie cookie, final String commenturl)
				throws MalformedCookieException {
			if (cookie instanceof Cookie2) {
				Cookie2 cookie2 = (Cookie2) cookie;
				cookie2.setCommentURL(commenturl);
			}
		}

		public void validate(final Cookie cookie, final CookieOrigin origin)
				throws MalformedCookieException {
		}

		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			return true;
		}

	}

	/**
	 * <tt>"Discard"</tt> cookie attribute handler for RFC 2965 cookie spec.
	 */
	private class CookieDiscardAttributeHandler implements
			CookieAttributeHandler {

		public void parse(final Cookie cookie, final String commenturl)
				throws MalformedCookieException {
			if (cookie instanceof Cookie2) {
				Cookie2 cookie2 = (Cookie2) cookie;
				cookie2.setDiscard(true);
			}
		}

		public void validate(final Cookie cookie, final CookieOrigin origin)
				throws MalformedCookieException {
		}

		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			return true;
		}

	}

	/**
	 * <tt>"Version"</tt> cookie attribute handler for RFC 2965 cookie spec.
	 */
	private class Cookie2VersionAttributeHandler implements
			CookieAttributeHandler {

		/**
		 * Parse cookie version attribute.
		 */
		public void parse(final Cookie cookie, final String value)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (cookie instanceof Cookie2) {
				Cookie2 cookie2 = (Cookie2) cookie;
				if (value == null) {
					throw new MalformedCookieException(
							"Missing value for version attribute");
				}
				int version = -1;
				try {
					version = Integer.parseInt(value);
				} catch (NumberFormatException e) {
					version = -1;
				}
				if (version < 0) {
					throw new MalformedCookieException(
							"Invalid cookie version.");
				}
				cookie2.setVersion(version);
				cookie2.setVersionAttributeSpecified(true);
			}
		}

		/**
		 * validate cookie version attribute. Version attribute is REQUIRED.
		 */
		public void validate(final Cookie cookie, final CookieOrigin origin)
				throws MalformedCookieException {
			if (cookie == null) {
				throw new IllegalArgumentException("Cookie may not be null");
			}
			if (cookie instanceof Cookie2) {
				Cookie2 cookie2 = (Cookie2) cookie;
				if (!cookie2.isVersionAttributeSpecified()) {
					throw new MalformedCookieException(
							"Violates RFC 2965. Version attribute is required.");
				}
			}
		}

		public boolean match(final Cookie cookie, final CookieOrigin origin) {
			return true;
		}

	}

	public int getVersion() {
		return 1;
	}

	public Header getVersionHeader() {
		ParameterFormatter formatter = new ParameterFormatter();
		StringBuffer buffer = new StringBuffer();
		formatter.format(buffer,
				new NameValuePair("$Version", Integer.toString(getVersion())));
		return new Header("Cookie2", buffer.toString(), true);
	}

}
